/*
 * This file is part of TradeHelper library.
 *
 * TradeHelper library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * TradeHelper library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with TradeHelper library.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.rag.managers;

import com.alee.utils.FileUtils;
import com.alee.utils.HtmlUtils;
import com.alee.utils.TextUtils;
import com.alee.utils.WebUtils;
import com.rag.core.*;
import com.rag.managers.forum.DataLoadListener;
import com.rag.utils.RagUtils;
import net.htmlparser.jericho.Element;
import net.htmlparser.jericho.HTMLElementName;
import net.htmlparser.jericho.Segment;
import net.htmlparser.jericho.Source;

import java.io.IOException;
import java.net.URL;
import java.util.*;

/**
 * User: mgarin Date: 05.04.12 Time: 17:16
 */

public class ForumManager
{
    public static final String forumAddress = "http://forum.motr-online.com/";
    public static final String sellAddress = forumAddress + "viewforum.php?f=15";
    public static final String buyAddress = forumAddress + "viewforum.php?f=27";
    public static final String tradeAddress = forumAddress + "viewforum.php?f=28";
    public static final String rentAddress = forumAddress + "viewforum.php?f=106";
    public static final String start = "&start=";

    public static final int topicsPerPage = 25;
    public static final int maxTopicTitleLemgth = 60;

    public static final int postsPerPage = 15;

    /**
     * Загружаем данные с форума
     */

    public static List<SingleTopicData> loadForumData ( ForumType forumType, int page,
                                                        DataLoadListener listener )
    {
        return loadForumData ( forumType, page, page, listener );
    }

    public static List<SingleTopicData> loadForumData ( ForumType forumType, int startPage,
                                                        int endPage, DataLoadListener listener )
    {
        // Обрабатываем все необходимые страницы
        int pageAmount = endPage - startPage + 1;
        float progress = 0f;
        final List<SingleTopicData> topicsData = new ArrayList<SingleTopicData> ();
        for ( int i = startPage; i <= endPage; i++ )
        {
            // Загружаем страницу
            listener.stateUpdated ( "Загружаем страницу №" + ( i + 1 ), progress );
            String page = loadForumPage ( forumType, i );
            progress += 50f / pageAmount;

            // Парсим страницу
            listener.stateUpdated ( "Парсим страницу №" + ( i + 1 ), progress );
            if ( page != null )
            {
                // Парсим HTML-содержимое страницы
                Source source = new Source ( page );
                source.fullSequentialParse ();

                // Проходимся по всем строкам всех таблиц
                int count = 0;
                for ( Element tr : source.getAllElements ( HTMLElementName.TR ) )
                {
                    // Проходимся по всем колонкам
                    int column = 0;
                    SingleTopicData topicData = null;
                    for ( Element td : tr.getAllElements ( HTMLElementName.TD ) )
                    {
                        if ( column == 0 )
                        {
                            Element img = td.getFirstElement ( HTMLElementName.IMG );
                            if ( img != null )
                            {
                                String src = img.getAttributeValue ( "src" );
                                if ( src != null && isProperTopicImage ( src ) )
                                {
                                    // This is proper topic image
                                    topicData = new SingleTopicData ();
                                    topicData.setMvpMark ( isMvpTopicImage ( src ) );
                                    if ( isClosedTopicImage ( src ) )
                                    {
                                        topicData.setTopicType ( TopicType.closed );
                                    }
                                }
                                else
                                {
                                    break;
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                        else if ( column == 1 )
                        {
                            // Парсим заголовок топика
                            Element a = td.getFirstElement ( HTMLElementName.A );
                            String link = a.getAttributeValue ( "href" );

                            // Парсим ID топика
                            topicData.setId ( RagUtils.parseTopicId ( link ) );

                            // Парсим ID форума
                            topicData.setForumId ( RagUtils.parseForumId ( link ) );

                            // Парсим название топика
                            topicData.setTopic ( a.getContent ().getRenderer ().toString () );

                            // Парсим тип топика
                            if ( topicData.getTopicType () == null )
                            {
                                topicData.setTopicType ( TopicType
                                        .getTopicType ( forumType, topicData.getTopic () ) );
                            }
                        }
                        else if ( column == 2 )
                        {
                            // Парсим данные об авторе топика
                            Element a = td.getFirstElement ( HTMLElementName.A );
                            String userLink = a.getAttributeValue ( "href" );
                            String author = a.getContent ().getRenderer ().toString ().trim ();
                            String id = RagUtils.parseUserId ( userLink );

                            // Ищем встречался ли автор ранее
                            User user = UsersManager.getUser ( id );
                            if ( user == null )
                            {
                                // Если нет, то создаём пользователя
                                UsersManager.cacheUser ( new User ( id, author ) );
                            }
                            else
                            {
                                // Если да, то просто обновляем имя
                                user.setName ( author );
                            }

                            // Сохраняем ID автора топика
                            topicData.setUserId ( id );
                        }
                        else if ( column == 3 )
                        {
                            // Парсим количество ответов на топик
                            Segment p = td.getFirstElement ( HTMLElementName.P ).getContent ();
                            topicData.setReplyCount ( Integer.parseInt ( p.toString () ) );
                        }
                        else if ( column == 5 )
                        {
                            // Парсим дату последнего обновления
                            Element p = td.getFirstElement ( HTMLElementName.P );
                            topicData.setUpdateDate (
                                    p.getContent ().getRenderer ().toString ().trim () );
                        }
                        column++;
                    }
                    if ( topicData != null )
                    {
                        // Парсим итемы из заголовков и содержимого топиков
                        topicData.setParsedItems ( parseSingleTopicData ( topicData.getTopic (),
                                topicData.getTopicType (), topicData.getUserId () ) );

                        // Заполняем отметку о том, что в топике скорее всего есть ещё итемы
                        topicData.setOthersMark ( hasOtherItemsInTopic ( topicData ) );

                        // Добавляем найденный топик с содержимым
                        topicsData.add ( topicData );

                        // Запоминаем кол-во прочитанных топиков
                        count++;

                        // Завершаем цикл, если прочитано необходимое кол-во топиков
                        if ( count == 25 )
                        {
                            break;
                        }
                    }
                }
            }
            progress += 50f / pageAmount;
        }

        // Обновляем сохраненённые в избранное топики
        FavoritesManager.updateFavoriteTopics ( topicsData );

        listener.stateUpdated ( "Загружаем отображение...", 100 );
        return topicsData;
    }

    private static List<String> moreText =
            Arrays.asList ( "и т.д.", "и тд", "и.т.д.", "и ещё", "и пр.", "и др.", "и хлам", "вещи",
                    "карты", "список", "etc", "e.t.c", "другие", "прочие", "прочее", "другое",
                    "другого", "дроп", "+++", "...", "внутри" );

    private static boolean hasOtherItemsInTopic ( SingleTopicData topicData )
    {
        String topicTitle = topicData.getTopic ().toLowerCase ();
        for ( String more : moreText )
        {
            if ( topicTitle.contains ( more ) ||
                    topicTitle.contains ( more.replaceAll ( " ", "" ) ) )
            {
                return true;
            }
        }
        return false;
    }

    private static boolean isProperTopicImage ( String src )
    {
        return src.contains ( "./styles/subsilver2/imageset/topic_read_hot.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/topic_unread_hot.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/topic_read.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/topic_unread.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/sticky_read.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/sticky_unread.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/topic_read_locked.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/topic_unread_locked.gif" );
    }

    private static boolean isClosedTopicImage ( String src )
    {
        return src.contains ( "./styles/subsilver2/imageset/topic_read_locked.gif" ) ||
                src.contains ( "./styles/subsilver2/imageset/topic_unread_locked.gif" );
    }

    private static boolean isMvpTopicImage ( String src )
    {
        // todo Check if there are MVP/miniboss items in the thread
        return src.contains ( "./styles/subsilver2/imageset/sticky_read.gif" );
    }

    /**
     * Загружаем форумную страницу
     */

    private static String loadForumPage ( ForumType forumType, int pageNumber )
    {
        try
        {
            URL url = new URL ( forumType.getAddress () + start + ( pageNumber * 25 ) );
            return FileUtils.readToString ( url.openStream () );
        }
        catch ( IOException e )
        {
            return null;
        }

        //        MultipartPostMethod pageLoad = new MultipartPostMethod ( forumType.getAddress () + start +
        //                ( pageNumber * 25 ) );
        //        try
        //        {
        //            // Производим авториацию
        //            HttpClient client = getHttpClient ();
        //            ProxyManager.setProxySettings ( client, pageLoad );
        //            int status = client.executeMethod ( pageLoad );
        //            if ( status == HttpStatus.SC_OK )
        //            {
        //                String page = pageLoad.getResponseBodyAsString ();
        //                pageLoad.releaseConnection ();
        //                return page;
        //            }
        //            else
        //            {
        //                return null;
        //            }
        //        }
        //        catch ( Throwable ex )
        //        {
        //            System.out.println ( ex.getMessage () );
        //        }
        //        finally
        //        {
        //            pageLoad.releaseConnection ();
        //        }
        //        return null;
    }

    /**
     * Парсим список итемов из данных о топике
     */

    private static List<Character> itemSeparators =
            Arrays.asList ( ',', '/', '|', '&', '=', '>', '\n', '\\' );
    private static List<String> itemTextSeparators =
            Arrays.asList ( " и ", " или ", " and ", " or ", " - ", " + ", "\n", "\\n", "\\\n" );
    private static Map<Character, String> innerSeparators = new HashMap<Character, String> ();

    static
    {
        innerSeparators.put ( '(', ")" );
        innerSeparators.put ( '[', "]" );
        innerSeparators.put ( '{', "}" );
        innerSeparators.put ( '<', ">" );
    }

    private static List<ItemData> parseSingleTopicData ( String topic, TopicType topicType,
                                                         String userId )
    {
        String topicTitle = topic.trim ().toLowerCase ();
        List<ItemData> foundData = new ArrayList<ItemData> ();

        // 1. Пропускаем ненужные типы топиков
        if ( topicType.equals ( TopicType.closed ) || topicType.equals ( TopicType.guild ) )
        {
            return foundData;
        }

        // 2. Удаляем пометку с типом топика
        String maxMark = null;
        for ( String mark : topicType.getTitles () )
        {
            if ( topicTitle.startsWith ( mark ) )
            {
                if ( maxMark != null )
                {
                    maxMark = mark.length () > maxMark.length () ? mark : maxMark;
                }
                else
                {
                    maxMark = mark;
                }
            }
        }
        if ( maxMark != null )
        {
            // Удаляем пометку
            topicTitle = topicTitle.replaceFirst ( maxMark, "" );

            // Обрезаем лишние пробелы после удаления пометки
            topicTitle = topicTitle.trim ();
        }

        // todo При НЕнахождении в названии (или картоках) разделителей - получать все варианты схожестей и сравнивать их, если совпадений между ними не найдено - выдавать все варианты

        // todo Корректно парсить приписки карточек

        // todo Парсить сеты по названиям - показывать все итемы сета

        // todo Парсить заточку от-до (+5-8 или +8-9 или +8/9 или +8,9)

        // todo Парсить схему заголовка "<cardName> можно в / в <itemName>"
        // todo Парсить схему (в любых названиях) "<name1> или / or / '/' <name2>"

        // todo Парсить чанты на статы на разных итемах (занести в алиасы?)

        // todo По предположительным карточкам определять выбор итема если он спорен (карточки в бутсы => бутсы итем)

        // todo Парсить информационные приписки, например к найтсигер мечам - red/blue

        // 3. Парсим название топика на логические части
        // Данные текущей части
        String name = "";
        boolean nameGoes = false;
        Integer refine = null;
        Integer slotCount = null;
        boolean foundInner = false;
        List<String> cards = null;
        // Переменные
        int i = 0;
        boolean innerPattEnded = false;
        while ( i < topicTitle.length () )
        {
            char currentChar = topicTitle.charAt ( i );

            // Закрываем текущий итем
            int separatorLength;
            boolean clearlySeparated;
            if ( itemSeparators.contains ( currentChar ) )
            {
                separatorLength = 1;
                clearlySeparated = true;
            }
            else
            {
                String ts = getTextSeparator ( topicTitle, i );
                if ( ts != null )
                {
                    separatorLength = ts.length ();
                    clearlySeparated = true;
                }
                else
                {
                    separatorLength = 0;
                    clearlySeparated = false;
                }
            }
            if ( clearlySeparated ||
                    isProperName ( name ) && isRefineStart ( currentChar ) && foundInner ||
                    !nameGoes && isProperName ( name ) && !isRefineStart ( currentChar ) &&
                            !isInnerStart ( currentChar ) )
            {
                // Очищаем от пробелов
                name = name.trim ();

                // Сохраняем текущие данные
                if ( isProperName ( name ) )
                {
                    // Добавляем временные данные об итеме если присутствует хотя бы имя
                    ItemData itemData = new ItemData ();

                    // Имя автора уникально, поэтому фигурирует как ID пользователя
                    itemData.setUserId ( userId );

                    // Полученное имя итема временно кладётся в ID для дальнейших изменений
                    itemData.setItemId ( name );

                    // При явном разделении насильно проставляем заточку, чтобы её не заменило
                    itemData.setRefineRate (
                            refine != null ? refine : ( clearlySeparated ? 0 : null ) );

                    // Вероятное количество слотов
                    itemData.setSlotCount ( slotCount );

                    // Находили ли мы какие-то указатели на содержимое
                    itemData.setFoundInner ( foundInner );

                    // ID карточек (внутренних итемов)
                    if ( cards != null && cards.size () > 0 )
                    {
                        itemData.setCardIds ( cards );
                    }

                    // Добавляем в найденные
                    foundData.add ( itemData );
                }
                else
                {
                    if ( foundData.size () > 0 )
                    {
                        ItemData prevData = foundData.get ( foundData.size () - 1 );
                        if ( refine != null && prevData.getRefineRate () == null )
                        {
                            // Устанавливаем заточку предыдущему итему, если данного найдено не было
                            prevData.setRefineRate ( refine );
                        }
                        if ( slotCount != null && prevData.getSlotCount () == null )
                        {
                            // Устанавливаем заточку предыдущему итему, если данного найдено не было
                            prevData.setSlotCount ( slotCount );
                        }
                        if ( cards != null && cards.size () > 0 && prevData.getCardIds () == null )
                        {
                            // Устанавливаем заточку предыдущему итему, если данного найдено не было
                            prevData.setCardIds ( cards );
                        }
                    }
                }

                // Очищаем данные
                name = "";
                nameGoes = false;
                refine = null;
                slotCount = null;
                foundInner = false;
                cards = null;

                // Идём далее
                i += separatorLength;
                continue;
            }

            // Пробуем распарсить заточку после знака "+"
            if ( isRefineStart ( currentChar ) )
            {
                // Название закончилось
                nameGoes = false;

                // Находим числовую часть
                int length = 0;
                for ( int j = i + 1; j < topicTitle.length (); j++ )
                {
                    try
                    {
                        // Пробуем получить заточку
                        refine = Integer.parseInt ( topicTitle.substring ( i + 1, j + 1 ) );
                        length++;
                    }
                    catch ( Throwable e )
                    {
                        // Более не парсится
                        break;
                    }
                }

                // Пропускаем часть с заточкой
                i += 1 + length;
            }

            // Начинаем считывать карты, либо кол-во слотов
            else if ( isInnerStart ( currentChar ) )
            {
                // Название закончилось
                nameGoes = false;

                // Помечаем что уже находили внутреннее содержание
                foundInner = true;

                // Находим конец внутренней части
                String endChar = innerSeparators.get ( currentChar );
                int innerEnd = topicTitle.indexOf ( endChar, i );
                if ( innerEnd == -1 )
                {
                    innerEnd = topicTitle.length () - 1;
                }

                // Выделяем внутреннюю часть
                String innerPart = topicTitle.substring ( i + 1, innerEnd );

                try
                {
                    // Возможно во внутренней части указано количество слотов итема
                    slotCount = Integer.parseInt ( innerPart );
                }
                catch ( Throwable e )
                {
                    // Если нет, значит там список итемов (в идеале карт/чантов)

                    // Создаём список, если его ещё не было
                    if ( cards == null )
                    {
                        cards = new ArrayList<String> ();
                    }

                    // Парсим внутреннюю часть в список     
                    String separator = null;
                    // Чаровые сепараторы
                    for ( Character sc : itemSeparators )
                    {
                        String ss = "" + sc;
                        if ( innerPart.contains ( ss ) )
                        {
                            separator = ss;
                            break;
                        }
                    }
                    // Стринговые сепараторы
                    for ( String ss : itemTextSeparators )
                    {
                        if ( innerPart.contains ( ss ) )
                        {
                            separator = ss;
                            break;
                        }
                    }
                    if ( separator != null )
                    {
                        StringTokenizer st = new StringTokenizer ( innerPart, separator, false );
                        while ( st.hasMoreTokens () )
                        {
                            cards.add ( st.nextToken ().trim () );
                        }
                    }
                    else
                    {
                        cards.add ( innerPart );
                    }

                    // Находим указания на множественность карт
                    Map<Integer, Integer> toClone = new HashMap<Integer, Integer> ();
                    for ( int c = 0; c < cards.size (); c++ )
                    {
                        // Находим первое число в строке с именем
                        String card = cards.get ( c );
                        Integer amount = TextUtils.findFirstNumber ( card );

                        // Разумно ограничиваем количество карт максимальным кол-вом слотов
                        if ( amount != null && amount > 1 && amount <= 4 )
                        {
                            // "Размножаем" карту
                            toClone.put ( c, amount );
                        }
                    }
                    // Дублируем карты
                    for ( int c = cards.size () - 1; c >= 0; c-- )
                    {
                        if ( toClone.containsKey ( c ) )
                        {
                            int amount = toClone.get ( c );
                            String card = cards.get ( c );
                            for ( int j = 1; j < amount; j++ )
                            {
                                cards.add ( c, card );
                            }
                        }
                    }
                }

                // Пропускаем внутреннюю часть
                i = innerEnd + 1;
            }

            // Наращиваем название итема
            else
            {
                // Идёт название
                nameGoes = true;

                name = name.concat ( topicTitle.substring ( i, i + 1 ) );
                i++;
            }
        }
        // Сохраняем данные о последнем итеме
        if ( isProperName ( name ) )
        {
            // Добавляем временные данные о последнем итеме если присутствует хотя бы имя
            ItemData itemData = new ItemData ();
            itemData.setUserId ( userId );
            itemData.setItemId ( name );
            itemData.setRefineRate ( refine );
            itemData.setSlotCount ( slotCount );
            if ( cards != null && cards.size () > 0 )
            {
                itemData.setCardIds ( cards );
            }
            foundData.add ( itemData );
        }
        else
        {
            if ( foundData.size () > 0 )
            {
                ItemData prevData = foundData.get ( foundData.size () - 1 );
                if ( refine != null && prevData.getRefineRate () == null )
                {
                    // Устанавливаем заточку предыдущему итему, если данного найдено не было
                    prevData.setRefineRate ( refine );
                }
                if ( slotCount != null && prevData.getSlotCount () == null )
                {
                    // Устанавливаем заточку предыдущему итему, если данного найдено не было
                    prevData.setSlotCount ( slotCount );
                }
                if ( cards != null && cards.size () > 0 && prevData.getCardIds () == null )
                {
                    // Устанавливаем заточку предыдущему итему, если данного найдено не было
                    prevData.setCardIds ( cards );
                }
            }
        }

        // 4. Находим возможные ID итемов
        for ( ItemData itemData : foundData )
        {
            String unparsedName = itemData.getItemId ();
            List<String> possibleItemIds = new ArrayList<String> ();
            int maxSynonym = 0;

            List<String> unparsedCards = itemData.getCardIds ();
            Map<Integer, List<String>> possibleCardIds = new HashMap<Integer, List<String>> ();
            int[] maxSynonyms = new int[ unparsedCards != null ? unparsedCards.size () : 0 ];

            // Пробегаемся по всей базе итемов
            for ( Item item : RagManager.getKnownItems ().getValues () )
            {
                // Проходимся по всем синонимам названия итема
                for ( String synonym : RagManager.getItemSynonyms ( item ) )
                {
                    String slc = synonym.toLowerCase ();

                    // Проверяем основной итем
                    if ( synonym.length () >= maxSynonym && ( unparsedName.contains ( slc ) ||
                            unparsedName.replaceAll ( " ", "" ).contains ( slc ) ) )
                    {
                        if ( synonym.length () > maxSynonym )
                        {
                            possibleItemIds.clear ();
                        }
                        if ( !possibleItemIds.contains ( item.getId () ) )
                        {
                            possibleItemIds.add ( item.getId () );
                        }
                        maxSynonym = synonym.length ();
                    }

                    // Проверяем вложенные итемы
                    for ( int j = 0; j < maxSynonyms.length; j++ )
                    {
                        if ( synonym.length () >= maxSynonyms[ j ] &&
                                ( unparsedCards.get ( j ).contains ( slc ) ||
                                        unparsedCards.get ( j ).replaceAll ( " ", "" )
                                                .contains ( slc ) ) )
                        {
                            // Получаем или создаём список возможных ID если его нет
                            List<String> possibleItemCards;
                            if ( possibleCardIds.containsKey ( j ) )
                            {
                                possibleItemCards = possibleCardIds.get ( j );
                            }
                            else
                            {
                                possibleItemCards = new ArrayList<String> ();
                                possibleCardIds.put ( j, possibleItemCards );
                            }

                            if ( synonym.length () > maxSynonyms[ j ] )
                            {
                                possibleItemCards.clear ();
                            }
                            if ( !possibleItemCards.contains ( item.getId () ) )
                            {
                                possibleItemCards.add ( item.getId () );
                            }
                            maxSynonyms[ j ] = synonym.length ();
                        }
                    }
                }
            }

            // Сохраняем полученные данные
            if ( possibleItemIds.size () > 0 )
            {
                // Пометка о том, что были найдены возможные ID итемов
                itemData.setIdentified ( true );

                // Список ID возможных итемов 
                itemData.setPossibleItemIds ( possibleItemIds );

                // Список ID возможных карт
                itemData.setPossibleCardIds ( null );
                if ( possibleCardIds.size () > 0 )
                {
                    itemData.setPossibleCardIds ( possibleCardIds );
                }
            }
        }

        // todo При перемене местами карт с итемами найденную заточку карт превращать в их кол-во

        // 5. Меняем местами итемы и карты, если итемы были в скобках
        Map<ItemData, List<ItemData>> toReplace = new HashMap<ItemData, List<ItemData>> ();
        for ( ItemData itemData : foundData )
        {
            List<String> possibleItemIds = itemData.getPossibleItemIds ();
            Map<Integer, List<String>> possibleCardIds = itemData.getPossibleCardIds ();

            // Проверяем являются ли все "итемы" картами
            boolean allCards = true;
            if ( possibleItemIds != null )
            {
                for ( String id : possibleItemIds )
                {
                    if ( !RagUtils.isCard ( id ) )
                    {
                        allCards = false;
                        break;
                    }
                }
            }
            //            else {
            //                  allCards = false;
            //            }
            if ( !allCards )
            {
                continue;
            }

            // Проверяем являются ли все "карты" итемами
            boolean allItems = possibleCardIds != null;
            if ( possibleCardIds != null )
            {
                for ( List<String> possibleCards : possibleCardIds.values () )
                {
                    for ( String id : possibleCards )
                    {
                        if ( RagUtils.isCard ( id ) )
                        {
                            allItems = false;
                            break;
                        }
                    }
                    if ( !allItems )
                    {
                        break;
                    }
                }
            }
            if ( !allItems )
            {
                continue;
            }

            // Раз мы добрались сюда - значит нужно менять карты и итемы местами
            // Т.к. итемов может быть много (например в скобках были указаны несколько итемов для карты)
            // то мы делим один ItemData на несколько новых 
            List<ItemData> replacement = new ArrayList<ItemData> ();
            for ( int j = 0; j <= itemData.getCardIds ().size (); j++ )
            {
                List<String> possibleCards = possibleCardIds.get ( j );
                if ( possibleCards != null )
                {
                    ItemData newData = new ItemData ();
                    newData.setIdentified ( itemData.isIdentified () );
                    newData.setUserId ( itemData.getUserId () );

                    // Cards to item
                    newData.setItemId ( itemData.getCardIds ().get ( j ) );
                    newData.setPossibleItemIds ( possibleCards );

                    // Item to cards
                    newData.setCardIds ( Arrays.asList ( itemData.getItemId () ) );
                    Map<Integer, List<String>> pci = new HashMap<Integer, List<String>> ();
                    pci.put ( 0, itemData.getPossibleItemIds () );
                    newData.setPossibleCardIds ( pci );

                    // Добавляем в список на замену
                    replacement.add ( newData );
                }
            }
            toReplace.put ( itemData, replacement );
        }
        // Вставляем все полученные данные по тем же индексам
        for ( int j = foundData.size () - 1; j >= 0; j-- )
        {
            List<ItemData> replacement = toReplace.get ( foundData.get ( j ) );
            if ( replacement != null )
            {
                foundData.remove ( j );
                for ( ItemData newData : replacement )
                {
                    foundData.add ( j, newData );
                }
            }
        }

        // todo Если на следующий итем заточка не канает - значит она была к предыдущему

        // 6. Вносим логические изменения в полученный список итемов:
        // - Определяем по полученным "возможным" ID какой действительно итем должен быть использован
        // - Проставляем корректное кол-во слотов в итемы
        // - Выбираем наиболее подходящие итемы из списка возможных
        for ( int j = 0; j < foundData.size (); j++ )
        {
            ItemData itemData = foundData.get ( j );

            // Обрабатываем итем только если он был определён
            if ( itemData.getPossibleItemIds () != null )
            {
                // Предположительное количество слотов
                int possibleSlotCount =
                        itemData.getSlotCount () != null ? itemData.getSlotCount () :
                                ( itemData.getPossibleCardIds () != null ?
                                        itemData.getPossibleCardIds ().size () :
                                        ( itemData.isFoundInner () ? 1 : 0 ) );

                // Вычисляем итоговый итем
                List<Item> items = RagManager.getItems ( itemData.getPossibleItemIds () );
                Item item = RagUtils.getClosestBySlotItem ( possibleSlotCount, items );
                itemData.setItemId ( item.getId () );
                itemData.setSlotCount ( item.getSlotCount () );

                // Добавляем карты только к слотовым итемам
                if ( itemData.getSlotCount () != null && itemData.getSlotCount () > 0 )
                {
                    // Вычисляем итоговые карты
                    List<String> cardIds = new ArrayList<String> ();
                    if ( itemData.getCardIds () != null && itemData.getPossibleCardIds () != null )
                    {
                        for ( int k = 0; k < itemData.getCardIds ().size (); k++ )
                        {
                            List<String> possibleCardIds = itemData.getPossibleCardIds ().get ( k );
                            if ( possibleCardIds != null )
                            {
                                cardIds.add ( RagUtils.getMinimumIdItem (
                                        RagManager.getItems ( possibleCardIds ) ).getId () );
                            }
                        }
                    }
                    itemData.setCardIds ( cardIds.size () > 0 ? cardIds : null );

                    cardIds = itemData.getCardIds ();
                    if ( cardIds != null )
                    {
                        // Удаляем излишние карты (например в итем с 1 слотом незачем 3 одинаковых карты)
                        // Также удаляем карты, не подходящие под тип итема как некорректно определённые
                        int slots = itemData.getSlotCount ();
                        List<Integer> toRemove = new ArrayList<Integer> ();
                        Map<String, Integer> count = new HashMap<String, Integer> ();
                        for ( String id : cardIds )
                        {
                            int oldCount = 0;
                            if ( count.containsKey ( id ) )
                            {
                                oldCount = count.get ( id );
                            }
                            count.put ( id, oldCount + 1 );
                        }
                        ItemType itemType = item.getItemType ();
                        ItemClass itemClass = item.getItemClass ();
                        for ( int k = cardIds.size () - 1; k >= 0; k-- )
                        {
                            String id = cardIds.get ( k );
                            Integer idCount = count.get ( id );
                            if ( idCount > slots )
                            {
                                cardIds.remove ( k );
                                count.put ( id, idCount - 1 );
                            }
                            else
                            {
                                Item card = RagManager.getItem ( id );
                                if ( card.getItemClass ().equals ( ItemClass.weaponCard ) )
                                {
                                    if ( !itemType.equals ( ItemType.weapon ) )
                                    {
                                        cardIds.remove ( k );
                                        count.put ( id, idCount - 1 );
                                    }
                                }
                                else if ( card.getItemClass ().equals ( ItemClass.headgearCard ) )
                                {
                                    if ( !ItemClass.getHeadgearClasses ().contains ( itemClass ) )
                                    {
                                        cardIds.remove ( k );
                                        count.put ( id, idCount - 1 );
                                    }
                                }
                                else if ( card.getItemClass ().equals ( ItemClass.armorCard ) )
                                {
                                    if ( !itemClass.equals ( ItemClass.armor ) )
                                    {
                                        cardIds.remove ( k );
                                        count.put ( id, idCount - 1 );
                                    }
                                }
                                else if ( card.getItemClass ().equals ( ItemClass.shieldCard ) )
                                {
                                    if ( !itemClass.equals ( ItemClass.shield ) )
                                    {
                                        cardIds.remove ( k );
                                        count.put ( id, idCount - 1 );
                                    }
                                }
                                else if ( card.getItemClass ().equals ( ItemClass.garmentCard ) )
                                {
                                    if ( !itemClass.equals ( ItemClass.garment ) )
                                    {
                                        cardIds.remove ( k );
                                        count.put ( id, idCount - 1 );
                                    }
                                }
                                else if ( card.getItemClass ().equals ( ItemClass.accessoryCard ) )
                                {
                                    if ( !itemClass.equals ( ItemClass.accessory ) )
                                    {
                                        cardIds.remove ( k );
                                        count.put ( id, idCount - 1 );
                                    }
                                }
                                else if ( card.getItemClass ().equals ( ItemClass.footgearCard ) )
                                {
                                    if ( !itemClass.equals ( ItemClass.footgear ) )
                                    {
                                        cardIds.remove ( k );
                                        count.put ( id, idCount - 1 );
                                    }
                                }
                            }
                        }

                        for ( int k = cardIds.size () - 1; k >= 0; k-- )
                        {
                        }
                    }
                }
                else
                {
                    // В неслотовые итемы карты не идут
                    itemData.setCardIds ( null );
                    itemData.setPossibleCardIds ( null );
                }
            }

            // Переносим заточку при её несовместимости с данным итемом
            if ( itemData.getRefineRate () != null )
            {
                Item item = RagManager.getItem ( itemData.getItemId () );
                if ( item == null || !RagUtils.canBeRefined ( item ) )
                {
                    // Переносим заточку на следующий или предыдущий итем
                    ItemData prev = j > 0 ? foundData.get ( j - 1 ) : null;
                    ItemData next = j < foundData.size () - 1 ? foundData.get ( j + 1 ) : null;
                    if ( next != null &&
                            RagUtils.canBeRefined ( next.getItemId () ) &&
                            next.getRefineRate () == null )
                    {
                        next.setRefineRate ( itemData.getRefineRate () );
                    }
                    else if ( prev != null && RagUtils.canBeRefined ( prev.getItemId () ) &&
                            prev.getRefineRate () == null )
                    {
                        prev.setRefineRate ( itemData.getRefineRate () );
                    }

                    // Обнуляем заточку данного
                    itemData.setRefineRate ( null );
                }
            }
        }

        // 7. Удаляем неопознанные итемы
        for ( int j = foundData.size () - 1; j >= 0; j-- )
        {
            if ( !foundData.get ( j ).isIdentified () )
            {
                foundData.remove ( j );
            }
        }

        return foundData;
    }

    private static String getTextSeparator ( String topicTitle, int i )
    {
        String part = topicTitle.substring ( i );
        for ( String ss : itemTextSeparators )
        {
            if ( part.startsWith ( ss ) )
            {
                return ss;
            }
        }
        return null;
    }

    private static boolean isInnerStart ( char currentChar )
    {
        return innerSeparators.containsKey ( currentChar );
    }

    private static boolean isProperName ( String name )
    {
        return !name.trim ().equals ( "" );
    }

    private static boolean isRefineStart ( char currentChar )
    {
        return currentChar == '+';
    }

    /**
     * Загружаем страницу топика
     */

    public static FullTopicData loadFullTopicData ( SingleTopicData topic,
                                                    DataLoadListener listener )
    {
        listener.stateUpdated ( "Загружаем топик...", 0f );

        FullTopicData fullTopicData = new FullTopicData ();
        fullTopicData.setTopicData ( topic );

        boolean hasMorePages = true;
        int pageNumber = 0;
        int postsCount = 1;
        while ( pageNumber < getPagesCount ( postsCount ) )
        {
            String page = loadTopicPage ( topic, pageNumber );
            if ( page != null )
            {
                // Парсим HTML-содержимое страницы
                Source source = new Source ( page );
                source.fullSequentialParse ();

                for ( Element div : source.getAllElements ( HTMLElementName.DIV ) )
                {
                    String id = div.getAttributeValue ( "id" );
                    if ( id != null && id.equals ( "pagecontent" ) )
                    {
                        // Находим кол-во постов в топике
                        if ( pageNumber == 0 )
                        {
                            Element navTable = div.getFirstElement ( HTMLElementName.TABLE );
                            for ( Element navTd : navTable.getAllElements ( HTMLElementName.TD ) )
                            {
                                String content = navTd.getContent ().toString ();
                                if ( content != null && content.contains ( "Сообщений:" ) )
                                {
                                    postsCount = Integer.parseInt (
                                            content.substring ( content.indexOf ( ":" ) + 2,
                                                    content.indexOf ( "]" ) - 1 ) );
                                }
                            }
                        }

                        // Проходимся по всем сообщениям страницы
                        int count = 0;
                        List<PostData> answers = new ArrayList<PostData> ();
                        for ( Element msgTable : div.getAllElements ( HTMLElementName.TABLE ) )
                        {
                            // Собираем все подходящие строки сообщения
                            List<Element> msgTr = new ArrayList<Element> ();
                            for ( Element tr : msgTable.getAllElements ( HTMLElementName.TR ) )
                            {
                                String trClass = tr.getAttributeValue ( "class" );
                                if ( trClass != null &&
                                        ( trClass.equals ( "row1" ) || trClass.equals ( "row2" ) ) )
                                {
                                    msgTr.add ( tr );
                                }
                            }

                            // Если кол-во строк верное, парсим сообщение
                            if ( msgTr.size () == 3 )
                            {
                                PostData postData = new PostData ();

                                /////////////////////////// 1 строка ///////////////////////////
                                // TR с ником автора, заголовком и датой сообщения

                                // Автор берётся позднее, ибо здесь нет его ID
                                // Парсим имя автора
                                String author =
                                        msgTr.get ( 0 ).getFirstElement ( HTMLElementName.TD )
                                                .getContent ().getRenderer ().toString ().trim ();
                                //                                    UsersManager.getUser (  )

                                // todo Парсить дату сообщения из этой  строки

                                /////////////////////////// 2 строка ///////////////////////////
                                // TR с инфой об авторе, аватаркой и сообщением
                                Element tr2 = msgTr.get ( 1 );

                                // Извлекаем аватарку
                                for ( Element ava : tr2.getAllElements ( HTMLElementName.IMG ) )
                                {
                                    String alt = ava.getAttributeValue ( "alt" );
                                    if ( alt != null && alt.equals ( "Аватара пользователя" ) )
                                    {
                                        postData.setAvatarUrl ( WebUtils.normalizeUrl (
                                                forumAddress + ava.getAttributeValue ( "src" ) ) );
                                        break;
                                    }
                                }

                                // Извлекаем сообщение
                                for ( Element post : tr2.getAllElements ( HTMLElementName.DIV ) )
                                {
                                    String divClass = post.getAttributeValue ( "class" );
                                    if ( divClass != null && divClass.equals ( "postbody" ) )
                                    {
                                        postData.setMessage ( post.getContent ().toString () );
                                        break;
                                    }
                                }

                                /////////////////////////// 3 строка ///////////////////////////
                                // TR с разными функциональными кнопками
                                Element tr3 = msgTr.get ( 2 );

                                for ( Element a : tr3.getAllElements ( HTMLElementName.A ) )
                                {
                                    String href = a.getAttributeValue ( "href" );
                                    if ( href.contains ( "memberlist.php?mode=viewprofile&u=" ) )
                                    {
                                        // Получаем ID пользователя на форуме
                                        String userId = RagUtils.parseUserId ( href );

                                        // Ищем кэшировался ли ранее данный пользователь
                                        User user = UsersManager.getUser ( userId );
                                        if ( user == null )
                                        {
                                            // Если нет, то создаём пользователя
                                            UsersManager.cacheUser ( new User ( userId, author ) );
                                        }
                                        else
                                        {
                                            // Если да, то просто обновляем имя
                                            user.setName ( author );
                                        }

                                        // Сохраняем ID автора поста
                                        postData.setUserId ( userId );
                                        break;
                                    }
                                }

                                ////////////////////////////////////////////////////////////////

                                // Добавляем извлечённый пост
                                answers.add ( postData );
                            }
                        }

                        // Сохраняем все найденные сообщения топика
                        fullTopicData.addPosts ( answers );
                    }
                }
            }
            pageNumber++;

            listener.stateUpdated (
                    "Загружаем страницу: " + pageNumber + "/" + getPagesCount ( postsCount ),
                    100f * pageNumber / getPagesCount ( postsCount ) );
        }

        // Распознаём итемы и прочие данные из топика
        listener.stateUpdated ( "Распознаём содержимое...", 100f );
        parseFullTopicData ( fullTopicData );

        // Возвращаем полученные данные
        listener.stateUpdated ( "Загружаем отображение...", 100f );
        return fullTopicData;
    }

    private static int getPagesCount ( int postsCount )
    {
        return postsCount / postsPerPage + ( postsCount % postsPerPage == 0 ? 0 : 1 );
    }

    private static String loadTopicPage ( SingleTopicData topic, int pageNumber )
    {
        try
        {
            URL url = new URL ( topic.getTopicLink () + "&start=" + pageNumber * postsPerPage );
            return FileUtils.readToString ( url.openStream () );
        }
        catch ( IOException e )
        {
            return null;
        }

        //        MultipartPostMethod pageLoad = new MultipartPostMethod (
        //                topic.getTopicLink () + "&start=" + pageNumber * postsPerPage );
        //        try
        //        {
        //            // Производим авториацию
        //            HttpClient client = getHttpClient ();
        //            ProxyManager.setProxySettings ( client, pageLoad );
        //            int status = client.executeMethod ( pageLoad );
        //            if ( status == HttpStatus.SC_OK )
        //            {
        //                String page = pageLoad.getResponseBodyAsString ();
        //                pageLoad.releaseConnection ();
        //                return page;
        //            }
        //            else
        //            {
        //                return null;
        //            }
        //        }
        //        catch ( Throwable ex )
        //        {
        //            System.out.println ( ex.getMessage () );
        //        }
        //        finally
        //        {
        //            pageLoad.releaseConnection ();
        //        }
        //        return null;
    }

    private static FullTopicData parseFullTopicData ( FullTopicData fullTopicData )
    {
        if ( fullTopicData.getPosts ().size () > 0 )
        {
            // Изначальные данные о топике
            SingleTopicData singleTopicData = fullTopicData.getTopicData ();

            // Получаем параметры топика
            String message = fullTopicData.getPosts ().get ( 0 ).getMessage ();
            String messageToParse = HtmlUtils.getPlainText ( message );
            String fullMessage = singleTopicData.getTopic () + "\n" + messageToParse;
            TopicType topicType = singleTopicData.getTopicType ();
            String userId = singleTopicData.getUserId ();

            // Парсим все итемы из заголовка и поста автора
            List<ItemData> parsedItems = parseSingleTopicData ( fullMessage, topicType, userId );
            singleTopicData.setParsedItems ( parsedItems );
        }

        return fullTopicData;
    }

    //    /**
    //     * Получение HTTP клиента
    //     */
    //
    //    public static HttpClient getHttpClient ()
    //    {
    //        HttpClient client = new HttpClient ();
    //        client.setConnectionTimeout ( 10000 );
    //        client.getParams ().setCookiePolicy ( CookiePolicy.BROWSER_COMPATIBILITY );
    //        return client;
    //    }
}